'''
Created on Sep 26, 2013

@author: dewey
'''
import os
import urllib
import random
import time
from PyQt4.QtCore import *
from PyQt4.QtGui import *

from qpynygeolib.maplayers import BitmapLayer

currentTimeMs = lambda: int(round(time.time() * 1000))

def randomUrl(listOfUrls):
    return listOfUrls[random.randint(0, len(listOfUrls)-1)]

def validImage(image):
    if image is not None:
        return image.height() > 0 and image.width() > 0
    else:
        return False

class TileFetcherManager(QThread):
    
        fileLocks = set()
        nextId = 0
        
        @staticmethod
        def nextThreadId():
            TileFetcherManager.nextId += 1
            return TileFetcherManager.nextId
    
        def __init__(self, layer, tileList, force, loadOnFinish=True):
            super(TileFetcherManager, self).__init__(layer)
            self.__force = force
            self.__tileList = tileList
            self.__layer = layer
            self.loadOnFinish = loadOnFinish
            self.__cancelled = False
            self.__id = self.nextThreadId()
            
        def run(self):
            self.emit(SIGNAL("progress(int, int, int)"), self.threadId(), 0, len(self.__tileList))
            length = len(self.__tileList)
            #load cached tiles first, don't emit progress
            for index in range(length):
                tile = self.__tileList[index]
                if not self.__layer.loadedTiles.has_key(tile) and not self.isCancelled():
                    cachedFileName = self.__layer.cacheDir + self.__layer.fileName(tile)
                    self.load(None, cachedFileName, tile)
            
            for index in range(length):
                tile = self.__tileList[index]
                if not self.__layer.loadedTiles.has_key(tile) and not self.isCancelled():
                    cachedFileName = self.__layer.cacheDir + self.__layer.fileName(tile)
                    url = self.layer().randomTileUrl(tile)
                    self.load(url, cachedFileName, tile)
                self.emit(SIGNAL("progress(int, int, int)"), self.threadId(), index+1, length)
        
        def threadId(self):
            return self.__id

        def cancel(self):
            self.__cancelled = True
        
        def force(self):
            return self.__force
        
        def isCancelled(self):
            return self.__cancelled
        
        def layer(self):
            return self.__layer
        
        def load(self, url, imageFile, tile):
            if not imageFile in TileFetcherManager.fileLocks:
                TileFetcherManager.fileLocks.add(imageFile)
                image = None
            
                if os.path.isfile(imageFile):
                    image = QImage(imageFile)
                downloaded = False
                if not validImage(image) and self.layer().downloadTiles(tile[2], self.force()) and url is not None:
                    try:    
                        if urllib.urlretrieve(url, imageFile):
                            image = QImage(imageFile)
                    except IOError:
                        self.emit(SIGNAL("error(QString)"), QString(url))
                        return
                    downloaded = True
                
                if image is not None and self.loadOnFinish:
                    if validImage(image) and not self.isCancelled():
                        self.emit(SIGNAL("imageDownloaded(QImage, int, int, int)"), image, tile[0], tile[1], tile[2])
                TileFetcherManager.fileLocks.remove(imageFile)
                if self.__layer.pauseAfterDownload() > 0 and downloaded:
                    self.usleep(self.__layer.pauseAfterDownload())
            

class TiledBitmapLayer(BitmapLayer):
    """
    Signals:
    tilesLoading(QString) QString = layerId
    loadingComplete(QString) QString = layerId
    loadError(QString, QString) QString = layerId, QString = url (may be absent)"""
    
    MIN_REFRESH_INTERVAL = 200
    
    def __init__(self, layerId, tileType="tile", cacheDir=None, zIndex=0):
        super(TiledBitmapLayer, self).__init__(layerId, zIndex)
        
        if cacheDir is not None:
            self.setCacheDir(cacheDir)
        self.__currentTileZoom = -1
        self.__tileType = tileType
        self.loadedTiles = {}
        self.__downloadTiles = True
        self.__autoReload = True
        self.__lastLoaderThreadFired = 0
        self.__threadsOut = 0
    
    def tileType(self):
        return self.__tileType
    
    def setDownloadTiles(self, flag):
        self.__downloadTiles = flag
        
    def setAutoReload(self, flag):
        self.__autoReload = flag
    
    def downloadTiles(self, force=False, tileZoom=None):
        return self.__downloadTiles
    
    def autoReload(self, tileZoom=None):
        return self.__autoReload
    
    def setCacheDir(self, cacheDir):
        if not cacheDir.endswith(os.sep):
            cacheDir += os.sep
        self.cacheDir = cacheDir
    
    def type(self):
        return "MapFrameLayer|BitmapLayer|TiledBitmapLayer"
    
    def tileType(self):
        return self.__tileType
    
    def tileManagerThread(self, tileList, force):
        return TileFetcherManager(self, tileList, force)
    
    def loadImages(self, force=False):
        if self.visible() and (currentTimeMs() - self.__lastLoaderThreadFired) > self.MIN_REFRESH_INTERVAL:
            zoom = self.tileZoomFromMapZoom(self.frame().zoomLevel())
            if self.autoReload(zoom):
                if zoom != self.__currentTileZoom or force:
                    self.clear()
                    self.__currentTileZoom = zoom
        
        
                tileList = self.getTiles(self.frame().bounds(), zoom)
                self.cancelLoading()
                self.__lastLoaderThreadFired = currentTimeMs()
                managerThread = self.tileManagerThread(tileList, force)
                
                self.connect(managerThread, SIGNAL("finished()"), self.__threadIn)
                self.connect(managerThread, SIGNAL("finished()"), managerThread, SLOT("deleteLater()"))
                self.connect(managerThread, SIGNAL("error(QString)"), self.onTileLoadError)
                self.connect(managerThread, SIGNAL("imageDownloaded(QImage, int, int, int)"), self.imageLoaded)
                self.connect(self, SIGNAL("cancelAllThreads()"), managerThread.cancel)
                self.__threadOut()
                managerThread.start()
    
    def __threadOut(self):
        if self.__threadsOut == 0:
            self.emit(SIGNAL("tilesLoading(QString)"), self.layerId())
        self.__threadsOut += 1
    
    def __threadIn(self):
        self.__threadsOut -= 1
        if self.__threadsOut == 0:
            self.emit(SIGNAL("loadingComplete(QString)"), self.layerId())
    
    def tileZoomFromMapZoom(self, mapZoom):
        zoom = round(mapZoom)
        if zoom > self.maxZoom():
                zoom = self.maxZoom()
        elif zoom < self.minZoom():
            zoom = self.minZoom()
        return zoom
        
    def reloadImages(self):
        self.loadImages(True)    
    
    def isLoading(self):
        return self.__threadsOut != 0
    
    def cancelLoading(self):
        self.emit(SIGNAL("cancelAllThreads()"))
    
    def setVisible(self, flag):
        if flag != self.visible():
            if not flag:
                self.clear()
                BitmapLayer.setVisible(self, flag)
            else:
                BitmapLayer.setVisible(self, flag)
                self.loadImages()
            
    
    def clear(self):
        BitmapLayer.clear(self)
        self.loadedTiles.clear()
        self.cancelLoading()
                
    def imageLoaded(self, image, tileX, tileY, zoom):
        if self.__currentTileZoom == zoom and self.frame() is not None:
            tile = (tileX, tileY, int(zoom))
            bounds = self.tileBounds(tile)
            itemId = self.addPixmapItem(QPixmap.fromImage(image), bounds)
            self.loadedTiles[tile] = itemId
        
    def onTileLoadError(self, url):
        self.emit(SIGNAL("loadError(QString, QString)"), self.layerId(), url)
        
    def fileName(self, tile):
        return "%s_%i_%i_%i.png" % (self.tileType(), tile[2], tile[0], tile[1])
    
    def randomTileUrl(self, tile):
        url = randomUrl(self.tileUrls(tile))
        return url
    
    def getTiles(self, bounds, zoom):
        raise NotImplementedError
    def tileBounds(self, tile):
        raise NotImplementedError
    def tileUrls(self, tile):
        raise NotImplementedError
    def minZoom(self):
        raise NotImplementedError
    def maxZoom(self):
        raise NotImplementedError
    def pauseAfterDownload(self):
        return 0